using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Red.Controls.Utils;
using Red.Controls.ViewModels;
using Red.Core;
using Red.Core.Extensions;
using Red.EngineData.View;

namespace Red.Controls.Shell
{
    /// <summary>
    ///     Base class for all resource-editing editors
    /// </summary>
    public abstract class RedEditorWindow : RedMainWindow
    {
        #region Private

        private bool _readyToClose = false; // This flag indicates whether we can close this window without loosing any data
        Dictionary<string, bool> _availableInteropFunctors = new Dictionary<string, bool>();

        #endregion Private

        #region Properties

        /// <summary>
        ///     Editor host application
        /// </summary>
        protected IEditorHost Host { get; set; }

        /// <summary>
        ///     Data specific to this instance of editor
        /// </summary>
        protected EditorInstanceData InstanceData { get; set; }

        /// <summary>
        ///     Data shared across all editors
        /// </summary>
        protected EditorSharedData SharedData { get; set; }

        #endregion Properties

        #region Events

        public EventHandler BackendInitialized;
        
        #endregion Events

        #region C-tor

        protected RedEditorWindow( IEditorHost host, EditorInstanceData instanceData, EditorSharedData sharedData )
        {
            Host = host;
            InstanceData = instanceData;
            SharedData = sharedData;
            CommandBindings.Add( new CommandBinding( UICommands.RestoreEngine, RestoreEngine_Executed ) );
            CommandBindings.Add( new CommandBinding( UICommands.ShowConsole, ShowConsole_Executed ) );
            CommandBindings.Add( new CommandBinding( UICommands.ShowEngineDebugger, ShowEngineDebugger_Executed ) );
            CommandBindings.Add( new CommandBinding( UICommands.ShowConfigEditor, ShowConfigEditor_Executed ) );
			CommandBindings.Add( new CommandBinding( UICommands.ShowEngineLogs, ShowEngineLogs_Executed ) );

            // Set root object as editor's DataContext
            if ( InstanceData.EditorKind == EditorKind.EngineResource )
            {
                DataContext = instanceData.RootEditorItem;
            }

            Initialized += RedEditorWindow_Initialized;
            IsEnabled = false;
            BackendInitialized += RedEditorWindow_BackendInitialized;
            Closed += RedEditorWindow_Closed;

            SharedData.EngineMonitor.OnConnectionLost += Monitor_ConnectionLost;
            SharedData.EngineMonitor.OnCrash += Monitor_Crash;
            SharedData.EngineMonitor.OnFine += Monitor_Fine;
        }

        #endregion C-tor

        #region Private methods

        public static string CalculateMD5Hash( string input )
        {
            MD5 md5 = MD5.Create();
            byte[] inputBytes = Encoding.ASCII.GetBytes( input );
            byte[] hash = md5.ComputeHash( inputBytes );
            var sb = new StringBuilder();

            for ( int i = 0; i < hash.Length; i++ )
                sb.Append( hash[i].ToString( "X2" ) );

            return sb.ToString();
        }
        private async Task HandleBackedEditorInitialized( string editorId )
        {
            InteropDataItem engineData = await SharedData.EngineObjectRoot;
            string editorRelativePath = $"depot.editors[\"{editorId}\"]";
            InteropDataItem engineEditor = (InteropDataItem)await engineData.DigUpSubItemAsync( editorRelativePath );
            InstanceData.RootEditorItem = engineEditor;

            // Plug-in some notifiers allowing to steer the editor from the engine

            await engineEditor.RegisterNotifierAsync( "_onActivate", ( item, evt ) =>
            {
                Activate();
            } );

            await engineEditor.RegisterNotifierAsync( "_onMinimize", ( item, evt ) =>
            {
                WindowState = WindowState.Minimized;
            } );

            await engineEditor.RegisterNotifierAsync( "_onMaximize", ( item, evt ) =>
            {
                WindowState = WindowState.Maximized;
            } );

            await engineEditor.RegisterNotifierAsync( "_onRefresh", async ( item, evt ) =>
            {
                await InstanceData.RootEditorItem.RebuildAsync();
            } );

            await engineEditor.RegisterNotifierAsync( "_onClose", ( item, evt ) =>
            {
                Close();
            } );

            // once the editor is fully initialized, we can set the data to be used by the UI
            DataContext = engineEditor;
        }

        private async Task<bool> InitializeEditorBackend()
        {
            var tcs = new TaskCompletionSource<bool>();

            string depotPath = InstanceData.InstanceContext;
            string editorId = CalculateMD5Hash( depotPath );
            string engineFilePath = $"depot.files[\"{depotPath}\"]";
            InteropDataItem engineData = await SharedData.EngineObjectRoot;
            InteropDataItem engineDepot = (InteropDataItem)await engineData.DigUpSubItemAsync( "depot" );
            InteropDataItem engineFile = (InteropDataItem)await engineData.DigUpSubItemAsync( engineFilePath );

            await engineDepot.RegisterNotifierAsync( "_newEditorCreationSuccess", async ( item, evt ) =>
            {
                if ( evt == editorId && tcs.Task.IsCompleted == false )
                {
                    await HandleBackedEditorInitialized( editorId );
                    tcs.SetResult( true );
                }
            } );

            await engineDepot.RegisterNotifierAsync( "_newEditorCreationFailed", ( item, evt ) =>
            {
                if ( evt == editorId && tcs.Task.IsCompleted == false )
                    tcs.SetResult( false );
            } );

            string handle = GetPreviewHandle()?.ToString();
            var success = await engineFile.ExecuteAsync<bool>( "open", editorId, handle ?? "0" );
            if ( success.Value )
            {
                // Do not set result yet - wait until one of above notifiers (success/failed) kicks in
            }
            else
                tcs.SetResult( false );

            return await tcs.Task;
        }

        private async Task<bool> SaveResource()
        {
            if ( InstanceData.RootEditorItem == null )
            {
                return false;
            }

            var saved = await InstanceData.RootEditorItem.ExecuteAsync<bool>( "save" );
            if ( !saved.Value )
            {
                RedMessageBox.Show( this, RedDialogType.Ok, "Couldn't save resource." );
            }
            return saved.Value;
        }

        private async Task HandleResourceClosing()
        {
            if ( InstanceData.RootEditorItem != null )
            {
                var isModified = await InstanceData.RootEditorItem.ExecuteAsync<bool>( "isModified" );
                if ( isModified.Value )
                {
                    if ( RedMessageBox.Show( this, RedDialogType.YesNo, "Resource was modified, but wasn't saved. Would you like to save it?", "Save resource" ) )
                    {
                        bool saved = await SaveResource();
                        if ( !saved )
                        {
                            return;
                        }
                    }
                }
            }

            _readyToClose = true;
            Close();
        }

        private void AddInteropButton( int idx, RoutedUICommand cmd, string iconPath, string funcPath, params object[] callArgs )
        {
            var button = new RedButton() { Command = cmd, Visibility = Visibility.Collapsed };
            if ( iconPath != null ) button.Icon = (Canvas)FindResource( iconPath );
            GetFrame().Toolbar.Insert( idx, button );

            var rootView = InstanceData.RootEditorItem.Data; // TODO: turn it into using DataItem directly
            CommandBindings.Add( new CommandBinding(
                cmd,
                ( s, e ) => ExecuteInterop( rootView, funcPath, callArgs ),
                ( s, e ) => { e.CanExecute = CanExecuteInterop( button, rootView, funcPath ); }
                ) );
        }

        private void SetupToolbar()
        {
            Frame frame = GetFrame();
            if ( frame == null )
            {
                return;
            }

            if ( InstanceData.EditorKind == EditorKind.EngineResource )
            {
                // Setup standard resource toolbar. Only commands available in the back-end will be visible as buttons
                AddInteropButton( 0, UICommands.SaveResource, null, "save" );
                AddInteropButton( 1, UICommands.EditUndo, null, "actions.undo" );
                AddInteropButton( 2, UICommands.EditRedo, null, "actions.redo" );
                frame.Toolbar.Insert( 3, new Separator() );
                AddInteropButton( 4, ApplicationCommands.Copy, "appbar_page_copy", "preview.objects.copy" );
                AddInteropButton( 5, ApplicationCommands.Cut, "appbar_scissor", "preview.objects.cut" );
                AddInteropButton( 6, ApplicationCommands.Paste, "appbar_clipboard_paste", "preview.objects.paste", "" );
                AddInteropButton( 7, ApplicationCommands.Delete, "appbar_delete", "preview.objects.delete" );
                frame.Toolbar.Insert( 8, new Separator() );

                frame.Toolbar.Insert( 0, new SourceControlButton()
                {
                    ResourcePath = SharedData.DepotInfo.GetAbsolutePath( InstanceData.InstanceContext ),
                    DepotInfo = SharedData.DepotInfo
                } );
            }
        }

        private void SetupTitle()
        {
            // add editor name to the title
            Title = InstanceData.EditorName;

            if ( InstanceData.EditorKind != EditorKind.Custom )
            {
                // add currently edited path to the title
                Title += $" - {InstanceData.InstanceContext}";
            }
        }

        private void ExecuteInterop( DataView data, string command, params object[] callArgs )
        {
            Mouse.OverrideCursor = Cursors.Wait;
            data.Execute( Interop.MakeCall( command, callArgs ),
                res =>
                {
                    Mouse.OverrideCursor = null;
                },
                ex =>
                {
                    ShowErrorAsync( ex.Message ).DoNotAwait();
                    Mouse.OverrideCursor = null;
                } );
        }

        private bool CanExecuteInterop( FrameworkElement element, DataView data, string command )
        {
            if ( !_availableInteropFunctors.ContainsKey( command ) )
            {
                _availableInteropFunctors.Add( command, false );
                data.HasChild( command,
                    () =>
                    {
                        element.Visibility = Visibility.Visible;
                        _availableInteropFunctors[command] = true;
                        CommandManager.InvalidateRequerySuggested();
                    },
                    () =>
                    {
                        element.Visibility = Visibility.Collapsed;
                        _availableInteropFunctors[command] = false;
                    } );
            }

            return _availableInteropFunctors[command];
        }

        #endregion Private methods

        #region Event handlers

        private async void RedEditorWindow_Initialized( object sender, EventArgs e )
        {
            if ( InstanceData.EditorKind == EditorKind.EngineResource )
            {
                if ( await InitializeEditorBackend() )
                {
                    BackendInitialized?.Invoke( this, new EventArgs() );
                    CommandManager.InvalidateRequerySuggested();
                }
                else
                    await ShowErrorAsync( "Cannot open engine-side editor object", true );
            }
            else
                // No backend to initialize, so just call a handler for consistency
                await Dispatcher.BeginInvoke( (Action)( () =>
                {   // NOTE: queue this call, because here it would be called from editor constructor's InitializeComponent, and that would mean
                    // it would require to attach BackendInitialized _before_ InitializeComponent to work properly - easy to miss.
                    BackendInitialized?.Invoke( this, new EventArgs() );
                } ) );
        }

        private void RedEditorWindow_BackendInitialized( object sender, EventArgs e )
        {
            IsEnabled = true;
            SetupTitle();
            SetupToolbar();
        }

        protected override async void OnClosing( CancelEventArgs e )
        {
            if ( !_readyToClose && InstanceData.RootEditorItem != null )
            {
                e.Cancel = true;
                await HandleResourceClosing();
                return;
            }

            base.OnClosing( e );
        }

        private void RedEditorWindow_Closed( object sender, EventArgs e )
        {
            SharedData.EngineMonitor.OnConnectionLost -= Monitor_ConnectionLost;
            SharedData.EngineMonitor.OnCrash -= Monitor_Crash;
            SharedData.EngineMonitor.OnFine -= Monitor_Fine;
        }

        private void Monitor_ConnectionLost()
        {
            Dispatcher.BeginInvoke( (Action)( () =>
                GetFrame()?.PreviewPanel?.ShowError( 
                    "Connection lost", "Try to restore the engine", "Connection with engine was closed. Restart editor or restore the engine" 
                ) ) );
        }

        private void Monitor_Crash( string crashLog )
        {
            Dispatcher.BeginInvoke( (Action)( () =>
                GetFrame()?.PreviewPanel?.ShowError(
                    "Engine crashed", "Try to restore the engine.", crashLog
                ) ) );
        }

        private void Monitor_Fine()
        {
            Dispatcher.BeginInvoke( (Action)( () =>
                GetFrame()?.PreviewPanel?.HideError() 
                ) );
        }

        private async void RestoreEngine_Executed( object sender, EventArgs e )
        {
            await SharedData.EngineApp.Restore();
            SharedData.EngineMonitor.Enabled = true;
        }

        private async void ShowConsole_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var cmd = new CommandLineDictionary();
            cmd.Add( "root", ( InstanceData.EditorKind == EditorKind.EngineResource ) ? InstanceData.RootEditorItem.Data.Path : "" ); // TODO: turn it into using DataItem directly

            // pass the editor's context to allow the console to open itself separately for each editor
            string context = GetType().GetHashCode().ToString( "X" ) + "_" + InstanceData.InstanceContext;
            await Host.LaunchEditorAsync( "Red.Toolkit.Console", context, cmd, this );
        }

        private async void ShowEngineDebugger_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var cmd = new CommandLineDictionary();
            await Host.LaunchEditorAsync( "Red.Toolkit.EngineDebugEditor", "", cmd, this );
        }

        private async void ShowEngineLogs_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var cmd = new CommandLineDictionary();
            await Host.LaunchEditorAsync( "Red.Toolkit.EngineLogsViewer", "", cmd, this );
        }

        private async void ShowIsAssetBrowser_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            await Host.ShowResourceAsync( (string)e.Parameter );
        }

        private async void ShowConfigEditor_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var cmd = new CommandLineDictionary();
            await Host.LaunchEditorAsync( "Red.Toolkit.Config", "none", cmd, this );
        }

        #endregion Event handlers
    }
}